In [ ]:
from bokeh.io import show, output_notebook
output_notebook()

In [ ]:
import pandas as pd
import numpy as np
import datetime

from bokeh.models import (
    Plot, Line, ColumnDataSource,  DataRange1d, Range1d,
    LinearAxis, BasicTicker,
    DatetimeAxis, DatetimeTicker, DatetimeTickFormatter,
    BoxZoomTool, PanTool, ResetTool, WheelZoomTool, TapTool,
    HoverTool, Grid, Quad, Rect, Circle, Callback
)
from bokeh import palettes
from bokeh.properties import value
from bokeh.io import vplot

Process timelog


In [ ]:
from app.utils import timelog_path

raw = pd.read_table(timelog_path, quotechar=' ', sep=': ', names=['timestamp', 'activity'], engine='python',)

# Set the column types

raw.timestamp = pd.to_datetime(raw.timestamp)
raw = raw.drop_duplicates()

### Build the times
raw['formatted_time'] = raw.timestamp.apply(lambda x: x.strftime("%I %p"))  # Used for hover tools
raw['end'] = raw.timestamp
raw['start'] = raw['end'].shift(1)

raw['start'] = np.where(
    raw['activity'] == 'start',  # If the activity is start
    raw['timestamp'],  # Set the start to timestamp
    raw['start'],  # Else leave it as start
)

raw['delta'] = raw.end - raw.start
# Remove the non-work activities which all have ***
raw = raw[~raw.activity.str.contains('\*\*\*')]
# Boil down the categories to the main work categories
raw.activity = raw.activity.str.split(' ').str[0]

raw.head()

In [ ]:
# Build a dictionary of frames - one for each category

def make_dfs(raw):
    activities = list(raw.activity.unique())
    activities.remove('start')
    start_df = raw[raw.activity == 'start']
    nan_df = start_df.copy()
    nan_df['delta'] = np.NaN
    nan_df['activity'] = '_'

    dfs = {}

    for activity in activities:
        activity_df = raw[raw.activity == activity]

        # Add in the start rows with 0 deltas and do cumsum
        activity_df = activity_df.append(start_df)
        activity_df.sort('timestamp', inplace=True)
        activity_df['cumsum'] = np.cumsum(activity_df.groupby(activity_df.timestamp.dt.date)['delta'])
        activity_df['cumsum_hrs'] = activity_df['cumsum'].dt.seconds / (60 * 60)

        # Add in the nan rows so bokeh can plobt
        activity_df = activity_df.append(nan_df)
        activity_df.sort(['timestamp', 'activity'], inplace=True)

        dfs[activity] = ColumnDataSource(activity_df[['cumsum_hrs', 'timestamp']])
    return dfs, activities

dfs, cats = make_dfs(raw)

Plot it


In [ ]:
def make_plot(dfs, activities, x_range, plot_width=900):
    plot = Plot(
        x_range=x_range, 
        y_range=Range1d(0, 11), 
        background_fill='black', 
        border_fill='black',
        outline_line_color=None,
        plot_width=plot_width,
        plot_height=200,
        toolbar_location='left'
    )

    yticker = BasicTicker(min_interval=3)
    close_ticker = DatetimeTicker(desired_num_ticks=8)
    year_ticker = DatetimeTicker(desired_num_ticks=4)
    year_ticks = DatetimeTickFormatter(
        formats={
            'years': ["%Y"],
            'months': ["%Y"],
            'days': ["%Y"],
            'hours': ["%Y"]
        }
    )
    close_ticks = DatetimeTickFormatter(
        formats={
            'years': ["%b"],
            'months': ["%b"],
            'days': ["%a %d %b"],
            'hours': ["%I%p %d %b"]
        }
    )

    axis_properties = dict(
        major_label_text_color='white',
    )
    plot.add_layout(LinearAxis(ticker=yticker, **axis_properties), 'left')
    plot.add_layout(DatetimeAxis(formatter=close_ticks, ticker=close_ticker, **axis_properties), 'below')
    plot.add_layout(DatetimeAxis(formatter=year_ticks, ticker=year_ticker, **axis_properties), 'below')
    plot.add_layout(Grid(dimension=1, ticker=yticker, grid_line_alpha=0.3))

    palette = getattr(palettes, 'Spectral%s' % len(activities))

    for i, activity in enumerate(activities):
        source = dfs[activity]
        line = Line(
            line_color=palette[i], 
            line_join='round', line_cap='round', line_width=5, line_alpha=0.75,
            x='timestamp', y='cumsum_hrs'
        )
        plot.add_glyph(source, line)
        
    return plot

In [ ]:
# Some times

first_day = raw.loc[0, 'timestamp']
today = datetime.datetime.today()
one_week_ago = today - datetime.timedelta(weeks=1)
two_weeks_ago = today - datetime.timedelta(weeks=2)
one_week_forward = today + datetime.timedelta(weeks=1)

# The ranges
all_range   = Range1d(start=first_day, end=today)
month_range = Range1d(start=two_weeks_ago, end=one_week_forward)
week_range = Range1d(start=one_week_ago, end=today)

In [ ]:
highlight = Quad(
    left='start', right='end',  bottom=0, top=12,     
    fill_color='white', line_color='white', fill_alpha=0.2,
)
lowlight = Quad(
    left='start', right='end',  bottom=0, top=12,     
    fill_color='black', line_color='white', fill_alpha=0.5,
)

In [ ]:
#from IPython.display import Javascript
#Javascript('../app/static/javascripts/moment.min.js')

# MOMENT is already in ipython notebook!!

In [ ]:
all_plot = make_plot(dfs, cats, all_range)
detail_selection_source = ColumnDataSource(
    {'start': [all_range.start, month_range.end], 
     'end': [month_range.start, all_range.end]}
)
all_plot.add_glyph(detail_selection_source, lowlight)

In [ ]:
detail = make_plot(dfs, cats, month_range)
detail.add_tools(PanTool(dimensions=['width']))
detail.add_tools(WheelZoomTool(dimensions=['width']))
detail.add_tools(ResetTool())

week_selection_source =  ColumnDataSource({'start': [week_range.start], 'end': [week_range.end]})
detail.add_glyph(week_selection_source, highlight)



detail_code = """
    // Update the month selection box on the all_data plot when month pans
    var detail_selection_data = detail_selection_source.get('data');
    var detail_start = cb_obj.get('frame').get('x_range').get('start');
    var detail_end = cb_obj.get('frame').get('x_range').get('end');
    detail_selection_data['start'][1] = detail_end;
    detail_selection_data['end'][0] = detail_start;
    detail_selection_source.trigger('change');
    
    
    // Always make sure the week highlight box on detail is visible and centered
    var week_selection_data = week_selection_source.get('data');

    var x = moment.duration(detail_end - detail_start).asWeeks() / 2
    var start = moment(detail_start);
    
    var week_end = start.add(x, 'weeks').format('x');
    var week_start = start.add(1, 'weeks').format('x');  // Subtract 1 week because obj was updated

    week_selection_data['start'] = [week_start];
    week_selection_data['end'] = [week_end];
    week_selection_source.trigger('change');
    
    console.log('The week starts on ' + start.format());
"""

detail_xrange_callback = Callback(args={}, code=detail_code)
detail_xrange_callback.args['detail_selection_source'] = detail_selection_source
detail_xrange_callback.args['week_selection_source'] = week_selection_source
detail.x_range.callback = detail_xrange_callback

In [ ]:
show(vplot(all_plot, detail))

In [ ]: